/*
 * Copyright 2010 Gal Dolber.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.unnison.framework.rebind.binder;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JAnnotationType;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JPackage;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.event.shared.EventHandler;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import com.google.gwt.user.rebind.StringSourceWriter;

import com.unnison.framework.client.GuitEntryPoint;
import com.unnison.framework.client.binder.BinderContributorAnnotation;
import com.unnison.framework.client.binder.EventBusHandler;
import com.unnison.framework.client.binder.ViewInitializer;
import com.unnison.framework.client.eventbus.EventBus;
import com.unnison.framework.rebind.common.AbstractGenerator;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;

@Deprecated
public class EventBusBinderGenerator extends AbstractGenerator {

    protected static final String handlerRegistrationName = HandlerRegistration.class.getCanonicalName();

    protected static final String bindingsDeclaration = ArrayList.class.getCanonicalName() + "<" + handlerRegistrationName
        + "> bindings = new " + ArrayList.class.getCanonicalName() + "<" + handlerRegistrationName + ">();";

    protected HashMap<Class<? extends Annotation>, BinderContributor> contributors;

    protected JPackage domEventsPackage;
    protected JPackage sharedEventsPackage;
    protected JClassType gwtEventType;
    protected JClassType gwtHandlerType;

    protected String capitalize(String part) {
        return part.substring(0, 1).toUpperCase() + part.substring(1);
    }

    protected String eventClassNameToEventName(String simpleName) {
        String eventName;
        eventName = simpleName.substring(0, simpleName.length() - 5);
        eventName = eventName.substring(0, 1).toLowerCase() + eventName.substring(1);
        return eventName;
    }

    protected String eventNameToEventClassName(String eventName) {
        eventName = eventName.substring(0, 1).toUpperCase() + eventName.substring(1) + "Event";
        return eventName;
    }

    @SuppressWarnings("unchecked")
    protected void findContributors() throws UnableToCompleteException {
        if (contributors == null) {
            List<String> contributorsNames = getConfigurationProperty("app.binder.contributor").getValues();
            contributors = new HashMap<Class<? extends Annotation>, BinderContributor>();
            for (String c : contributorsNames) {
                JClassType cType = getType(c);
                if (cType == null) {
                    error("Error in binder contributor name. Found: %s", c);
                }

                JAnnotationType cAnnotation = cType.isAnnotation();
                if (cAnnotation == null) {
                    error("All binder contributors must be annotations. Found: %s", c);
                }

                if (!cAnnotation.isAnnotationPresent(BinderContributorAnnotation.class)) {
                    error("All binder contributors must be annotated with %s. Found: %s",
                        BinderContributorAnnotation.class.getCanonicalName(), c);
                }

                Class<?> binderContributor = cAnnotation.getAnnotation(BinderContributorAnnotation.class).value();
                if (!BinderContributor.class.isAssignableFrom(binderContributor)) {
                    error("All binder contributors must implement %s. Found: %s", BinderContributor.class.getCanonicalName(), c);
                }

                contributors.put((Class<? extends Annotation>) findClass(c), (BinderContributor) newInstance(binderContributor));
            }
        }
    }

    @Override
    protected void generate(SourceWriter writer) throws UnableToCompleteException {
        findContributors();

        // Do we need to check this?
        JClassType presenterType = baseClass.getImplementedInterfaces()[0].isParameterized().getTypeArgs()[0];

        // If it is an parameterized type we need to find the base type to get the right method names
        if (presenterType.isParameterized() != null) {
            presenterType = presenterType.isParameterized().getBaseType();
        }

        writer.println(bindingsDeclaration);

        writer.println("public void bind(final " + presenterType.getQualifiedSourceName() + " presenter, final "
            + EventBus.class.getCanonicalName() + " eventBus) {");
        writer.indent();
        printEventBusBindingMethods(writer, presenterType);
        printPresentersInitilizersCalls(writer, presenterType);
        writer.outdent();
        writer.println("}");

        writer.println("public void unbind() {");
        writer.indent();
        writer.println("for (" + handlerRegistrationName + " b : bindings) {b.removeHandler();}");
        writer.println("bindings.clear();");
        writer.outdent();
        writer.println("}");
    }

    /**
     * Find event in dom, shared or context locations. Context = '{currentPackage}/event'. Context events have priority
     * over shared and dom ones.
     */
    protected JClassType getEventByName(String eventName, JPackage contextEventsPackage) {
        // Get event class name
        eventName = eventNameToEventClassName(eventName);

        // Find in context
        if (contextEventsPackage != null) {
            JClassType contextEventType = contextEventsPackage.findType(eventName);
            if (contextEventType != null) {
                return contextEventType;
            }
        }

        // Find in dom events
        JClassType domEventType = domEventsPackage.findType(eventName);
        if (domEventType != null) {
            return domEventType;
        }

        // Find in shared events
        JClassType sharedEventType = sharedEventsPackage.findType(eventName);
        if (sharedEventType != null) {
            return sharedEventType;
        }

        return null;
    }

    /**
     * Retrieves the handler associated with the event.
     * @throws UnableToCompleteException
     */
    protected JClassType getHandlerForEvent(JClassType eventType) throws UnableToCompleteException {
        return eventType.findMethod("getAssociatedType", new JType[0]).getReturnType().isParameterized().getTypeArgs()[0];
    }

    /**
     * Finds the valid methods and check all the conventions.
     */
    protected void printEventBusBindingMethods(SourceWriter writer, JClassType presenterType) throws UnableToCompleteException {
        JPackage contextEventsPackage = getPackage(presenterType.getPackage().getName() + ".event");
        for (JMethod m : presenterType.getMethods()) {
            if (m.isAnnotationPresent(EventBusHandler.class)) {

                EventBusHandler eventBusHandler = m.getAnnotation(EventBusHandler.class);

                String name = m.getName();
                String presenterName = presenterType.getQualifiedSourceName();

                validateHandler(m, name, presenterName);

                // Find the event type
                JParameter[] parameters = m.getParameters();

                if (!name.startsWith("$") && !name.startsWith("eventBus$")) {
                    error("Bad method name: %s on class: %s, the method should start with '$' or 'eventBus$'", name, presenterName);
                }

                // Clean the name
                if (name.startsWith("$")) {
                    name = name.substring(1); // Cut off the $
                } else {
                    name = name.substring(9); // Cut off the eventBus$
                }

                JClassType eventType = getType(eventBusHandler.value().getCanonicalName());
                if (eventType.equals(gwtEventType)) {
                    eventType = getEventByName(name, contextEventsPackage);
                    if (eventType == null) {
                        error("There is no context, dom or shared event with the name '%s'. Binding method: '%s' in class: '%s'", name,
                            m.getName(), presenterType.getQualifiedSourceName());
                    }
                }

                StringBuilder bindingParameters = new StringBuilder();
                ArrayList<String> parameterStrings = new ArrayList<String>();
                for (JParameter p : parameters) {
                    String parameter = p.getName();

                    if (bindingParameters.length() > 0) {
                        bindingParameters.append(", ");
                    }

                    int initlenght = bindingParameters.length();

                    // Implicit cast
                    bindingParameters.append("(" + p.getType().getErasedType().getParameterizedQualifiedSourceName() + ")");

                    String getter = "get";

                    // Check if it is a boolean then the getter is 'is' not 'get'
                    JPrimitiveType parameterTypeIsPrimitive = p.getType().isPrimitive();
                    if (parameterTypeIsPrimitive != null && parameterTypeIsPrimitive.equals(JPrimitiveType.BOOLEAN)) {
                        getter = "is";
                    }

                    // Event getter binding
                    if (parameter.indexOf("$") == -1) {
                        // Event binding
                        bindingParameters.append("event.");
                        bindingParameters.append(getter);
                        bindingParameters.append(capitalize(parameter));
                        bindingParameters.append("()");
                    } else {
                        // Event binding anidated binding
                        String[] parameterParts = parameter.split("[$]");

                        bindingParameters.append("event");
                        for (int n = 0; n < parameterParts.length - 1; n++) {
                            bindingParameters.append(".get");
                            bindingParameters.append(capitalize(parameterParts[n]));
                            bindingParameters.append("()");
                        }

                        bindingParameters.append(".");
                        bindingParameters.append(getter);
                        bindingParameters.append(capitalize(parameterParts[parameterParts.length - 1]));
                        bindingParameters.append("()");
                    }

                    parameterStrings.add(bindingParameters.substring(initlenght, bindingParameters.length()));
                }

                // Find the event name
                String simpleName = eventType.getSimpleSourceName();
                if (!simpleName.endsWith("Event")) {
                    error("The event %s does not use the event convention. It should end with 'Event'", eventType.getQualifiedSourceName());
                }
                String eventName = eventClassNameToEventName(simpleName);

                // Check that the name of the event correspond to the method name convention
                if (!eventName.equals(name)) {
                    error("The method %s on class %s does not use the event bus handler method convention. "
                        + "It should start with '$' or 'eventBus$' "
                        + "and end with the event name. i.e ValueChangeEvent -> $valueChange. Solve it renaming it to '$%s'", m.getName(),
                        presenterName, eventName);
                }

                // Get event handler name
                JClassType handlerType = getHandlerForEvent(eventType);
                if (handlerType == null) {
                    error("Parameter '%s' is not an event (subclass of GwtEvent).", eventType.getName());
                }

                // Retrieves the single method (usually 'onSomething') related to all
                // handlers. Ex: onClick in ClickHandler, onBlur in BlurHandler ...
                JMethod[] methods = handlerType.getMethods();
                if (methods.length != 1) {
                    error("'%s' has more than one method defined.", handlerType.getName());
                }

                // 'onSomething' method
                JMethod handlerOnMethod = methods[0];

                // Checks if the method has an Event as parameter. Ex: ClickEvent in onClick, BlurEvent in onBlur ...
                parameters = handlerOnMethod.getParameters();
                if (parameters.length != 1) {
                    error("Method '%s' needs '%s' as parameter", handlerOnMethod.getName(), eventType.getName());
                }

                writer.println("bindings.add(eventBus.addHandler(new " + handlerType.getQualifiedSourceName() + "() {");
                writer.indent();
                writer.println("public void " + handlerOnMethod.getName() + "(final " + eventType.getQualifiedSourceName() + " event) {");
                writer.indent();

                // Process contributors
                String bindingParametersString = bindingParameters.toString();
                BinderContextImpl binderContext = processMethodContributors(presenterType, null, null, null, m, eventType,
                    parameterStrings.toArray(new String[parameterStrings.size()]));

                StringSourceWriter handlerWriter = new StringSourceWriter();

                handlerWriter.println("if (!GWT.isScript()) {");
                handlerWriter.println(GuitEntryPoint.class.getCanonicalName() + ".getLogger().initLog(\"EVENTBUS " + binderContext.getLog()
                    + "\");");
                handlerWriter.println("}");

                handlerWriter.println("presenter." + m.getName() + "(" + bindingParametersString + ");");

                handlerWriter.println("if (!GWT.isScript()) {");
                handlerWriter.println(GuitEntryPoint.class.getCanonicalName() + ".getLogger().endLog();");
                handlerWriter.println("}");

                writer.println(binderContext.build(handlerWriter));

                writer.outdent();
                writer.println("}");
                writer.outdent();
                writer.println("}, " + eventType.getQualifiedSourceName() + ".getType()));");
            }
        }
    }

    /**
     * Print calls to @PresenterInitializer methods.
     */
    protected void printPresentersInitilizersCalls(SourceWriter writer, JClassType presenterType) throws UnableToCompleteException {
        for (JMethod m : presenterType.getMethods()) {
            if (m.isAnnotationPresent(ViewInitializer.class)) {
                if (m.getParameters().length > 0) {
                    error("All @ViewInitializer must have zero parameters. Found: %s.%s", presenterType.getQualifiedSourceName(),
                        m.getName());
                }

                writer.println("presenter." + m.getName() + "();");
            }
        }

        JClassType superclass = presenterType.getSuperclass();
        if (superclass != null) {
            printPresentersInitilizersCalls(writer, superclass);
        }
    }

    @Override
    protected void processComposer(ClassSourceFileComposerFactory composer) {
        gwtEventType = getType(GwtEvent.class.getCanonicalName());
        gwtHandlerType = getType(EventHandler.class.getName());
        domEventsPackage = getPackage("com.google.gwt.event.dom.client");
        sharedEventsPackage = getPackage("com.google.gwt.event.logical.shared");
        composer.addImport(GWT.class.getCanonicalName());
        composer.addImplementedInterface(typeName);
    }

    protected BinderContextImpl processMethodContributors(JClassType presenterType, JClassType viewType, JClassType viewInterfaceType,
        String viewTypeName, JMethod method, JClassType eventType, String[] parameterGetters) throws UnableToCompleteException {
        BinderContextImpl binderContext = new BinderContextImpl(method, eventType, presenterType, viewType, viewInterfaceType,
            viewTypeName, parameterGetters);
        for (Entry<Class<? extends Annotation>, BinderContributor> entry : contributors.entrySet()) {
            if (method.isAnnotationPresent(entry.getKey())) {
                BinderContributor contributor = entry.getValue();
                binderContext.log(contributor.name());
                contributor.contribute(binderContext, logger, context);
            }
        }
        return binderContext;
    }

    protected void validateHandler(JMethod m, String name, String presenterName) throws UnableToCompleteException {
        if (m.isStatic()) {
            error("All event handlers must not be static. Found: %s.%s", presenterName, name);
        }

        if (m.isPrivate()) {
            error("All event handlers must not be private. Found: %s.%s", presenterName, name);
        }

        JPrimitiveType returnTypePrimitive = m.getReturnType().isPrimitive();
        if (returnTypePrimitive == null || !returnTypePrimitive.equals(JPrimitiveType.VOID)) {
            error("All event handlers should return void. Found: %s.%s", presenterName, name);
        }
    }
}
